<?php
namespace Process;
use Exception ;
include './C.php';
class P
{
    /**
     * 进程数
     * @var int
     */
    protected $process_num;

    /**
     * 进程名称
     * @var string
     */
    public $process_title ;

    /**
     * 被实例化次数
     * @var int
     */
    protected static $new_count = 0;

    /**
     * 执行模式
     * @var
     */
    protected $model ;


    /**
     * 子进程工作内存限制
     * @var int
     */
    public $memory_limit ;

    /**
     * work 进程列表
     * @var
     */
    public $process_list;

    /**
     * 程序运行日志文件
     * @var
     */
    public $log_file ;

    public $pid ;

    public $run_tmp_file_path;

    /**
     * Process constructor.
     * @param $process_num
     * @param $process_title
     * @param $callbacks
     * @param int $model 1代表一直执行 ， 0 代表只执行一次
     * @param int $memory_limit
     * @param bool $run_tmp_file_path
     * @throws Exception
     */
    public function __construct( $process_num , $process_title , $callbacks ,$model = 1 , $memory_limit =200 ,$run_tmp_file_path = false)
    {
        if (stripos(PHP_SAPI,'cli' ) === false)
        {
            throw new Exception('多进程模式请在cli模式下运行!');
        }
        $this->model = $model ;
        self::$new_count +=1;
        $this->memory_limit = $memory_limit ;
        $this->pid = getmypid() ;
        $this->process_title = "{$process_title}".self::$new_count;
        is_numeric( $process_num ) ?$this->process_num = $process_num  : $this->process_num = 1;
        $this->process_num > 0? : $this->process_num = 1 ;
        if(is_array($callbacks))
        {
            if(is_array(current($callbacks)))//类的回调方法
            {
                if(count($callbacks) < $this->process_num )
                {
                    throw new Exception('为每一个work 进程指定单独运行程序时，必须指定跟进程数相符数量的回调');
                }
                $this->callbacks = $callbacks ;
            }
            else // 普通的回调方法
            {
                for($i = 1 ; $i<=$this->process_num ; $i++ )
                {
                    $this->callbacks[] = $callbacks ;
                }
            }
        }
        else
        {
            if(!is_callable($callbacks))
            {
                throw new Exception('必须传入可调用类型');
            }
            for($i =1 ; $i<=$this->process_num ; $i++ )
            {
                $this->callbacks[] = $callbacks ;
            }
        }
        $this->run_tmp_file_path   =( $run_tmp_file_path? : __DIR__.'/run/');
        if(!is_dir($this->run_tmp_file_path))
        {
            try{
                mkdir($this->run_tmp_file_path,0777,true);
            }
            catch(Exception $e)
            {
                throw new Exception( 'pid 文件目录创建失败!' );
            }
        }
        $this->run_tmp_file_path = realpath($this->run_tmp_file_path);
        $this->log_file = $this->run_tmp_file_path."/{$this->process_title}_error.log" ;
    }

    /**
     * 开启进程和设置进程执行体
     * @param bool $repeat
     * @param float $sleep
     * @return bool
     */
    public function run($sleep = 0.1 )
    {
        # 父进程名称设置
        cli_set_process_title("{$this->process_title}: master");
        # 父进程信号处理
        $this->registerSignal();
        $this->run_param = array($repeat,$sleep);
        for ($i=0;$i<$this->process_num;$i++)
        {
            $this->process_list[$i] = pcntl_fork() ;
            if($this->process_list[$i]==0)
            {
                # 注册信号处理
                $this->registerSignal();
                $this->pid = getmypid();
                # 设置进程名称
                $this->processTitle("work{$i}");
                # 设置子进程执行体
                $this->run_param[3] = $this->callbacks[$i];
                $this->work_id      = $i;
                goto work ;
            }
            else if($this->process_list[$i] == -1)
            {
                goto error;
            }
        }
        master:
        $this->start();
        return true ;
        work:
        $this->work(...$this->run_param );
        exit;
        error:
        $this->stop();
        return false ;
    }

    /**
     * 子进程执行逻辑
     * @param $repeat
     * @param $sleep
     * @param $callback
     */
    protected function work($repeat, $sleep, $callback )
    {
        try{
            # 工作进程清理
            $this->cleanWork();
            do{
                call_user_func($callback);
                if($this->stop_signal)# 信号停止
                {
                    $this->last_error = "no error ! child process exited by normal!";
                    break;
                }
                if((memory_get_usage() / 1024 / 1024) >= $this->memory_limit) # 内存过大停止
                {
                    $this->last_error = "no error ! child process exited , use memory:[".(memory_get_usage() / 1024 / 1024)."] large than memory setting:[{$this->memory_limit}] ";
                    break;
                }
                $this->sleep($sleep);
            }while($repeat && $this->model );
        }
        catch(Exception|\RuntimeException $e)
        {
            #记录日志文件
            $this->last_error = " {$e->getMessage()} on file {$e->getFile()} in line {$e->getLine()} {$e->getTraceAsString()} {$e->getCode()}" ;
        }
    }

    protected function registerSignal()
    {
        pcntl_signal(SIGUSR2, array($this, 'signalHandle'));
        pcntl_signal(SIGUSR1, array($this, 'signalHandle'));
        pcntl_signal(SIGINT, array($this, 'signalHandle'));
        pcntl_signal(SIGTERM, array($this, 'signalHandle'));
    }

    /**
     * 增强sleep 可以小数
     * @param $sleep
     */
    public function sleep($sleep)
    {
        usleep($sleep*1000000);
    }
}